using System;
using System.IO;
using System.Runtime.InteropServices;


namespace Ultima {
  public class TileMatrix {
    private static HuedTileList[][] m_Lists;
    private readonly int m_BlockHeight;
    private readonly int m_BlockWidth;
    private readonly HuedTile[][][] m_EmptyStaticBlock;
    private readonly int m_Height;

    private readonly FileStream m_Index;
    private readonly BinaryReader m_IndexReader;
    private readonly Tile[] m_InvalidLandBlock;
    private readonly Tile[][][] m_LandTiles;
    private readonly FileStream m_Map;
    private readonly TileMatrixPatch m_Patch;

    private readonly FileStream m_Statics;
    private readonly HuedTile[][][][][] m_StaticTiles;

    private readonly int m_Width;

    public TileMatrix( int fileIndex, int mapID, int width, int height ) {
      this.m_Width = width;
      this.m_Height = height;
      this.m_BlockWidth = width >> 3;
      this.m_BlockHeight = height >> 3;

      if ( fileIndex != 0x7F ) {
        string mapPath = Client.GetFilePath( "map{0}.mul", fileIndex );

        if ( mapPath != null )
          this.m_Map = new FileStream( mapPath, FileMode.Open, FileAccess.Read, FileShare.Read );

        string indexPath = Client.GetFilePath( "staidx{0}.mul", fileIndex );

        if ( indexPath != null ) {
          this.m_Index = new FileStream( indexPath, FileMode.Open, FileAccess.Read, FileShare.Read );
          this.m_IndexReader = new BinaryReader( this.m_Index );
        }

        string staticsPath = Client.GetFilePath( "statics{0}.mul", fileIndex );

        if ( staticsPath != null )
          this.m_Statics = new FileStream( staticsPath, FileMode.Open, FileAccess.Read, FileShare.Read );
      }

      this.m_EmptyStaticBlock = new HuedTile[8][][];

      for ( int i = 0; i < 8; ++i ) {
        this.m_EmptyStaticBlock[i] = new HuedTile[8][];

        for ( int j = 0; j < 8; ++j )
          this.m_EmptyStaticBlock[i][j] = new HuedTile[0];
      }

      this.m_InvalidLandBlock = new Tile[196];

      this.m_LandTiles = new Tile[this.m_BlockWidth][][];
      this.m_StaticTiles = new HuedTile[this.m_BlockWidth][][][][];

      this.m_Patch = new TileMatrixPatch( this, mapID );

      /*for ( int i = 0; i < m_BlockWidth; ++i )
			{
				m_LandTiles[i] = new Tile[m_BlockHeight][];
				m_StaticTiles[i] = new Tile[m_BlockHeight][][][];
			}*/
    }

    public TileMatrixPatch Patch {
      get { return this.m_Patch; }
    }

    public int BlockWidth {
      get { return this.m_BlockWidth; }
    }

    public int BlockHeight {
      get { return this.m_BlockHeight; }
    }

    public int Width {
      get { return this.m_Width; }
    }

    public int Height {
      get { return this.m_Height; }
    }

    public HuedTile[][][] EmptyStaticBlock {
      get { return this.m_EmptyStaticBlock; }
    }

    [DllImport( "Kernel32" )]
    private static extern unsafe int _lread( IntPtr hFile, void* lpBuffer, int wBytes );

    public void SetStaticBlock( int x, int y, HuedTile[][][] value ) {
      if ( x < 0 || y < 0 || x >= this.m_BlockWidth || y >= this.m_BlockHeight )
        return;

      if ( this.m_StaticTiles[x] == null )
        this.m_StaticTiles[x] = new HuedTile[this.m_BlockHeight][][][];

      this.m_StaticTiles[x][y] = value;
    }

    public HuedTile[][][] GetStaticBlock( int x, int y ) {
      if ( x < 0 || y < 0 || x >= this.m_BlockWidth || y >= this.m_BlockHeight || this.m_Statics == null ||
          this.m_Index == null )
        return this.m_EmptyStaticBlock;

      if ( this.m_StaticTiles[x] == null )
        this.m_StaticTiles[x] = new HuedTile[this.m_BlockHeight][][][];

      HuedTile[][][] tiles = this.m_StaticTiles[x][y];

      if ( tiles == null )
        tiles = this.m_StaticTiles[x][y] = ReadStaticBlock( x, y );

      return tiles;
    }

    public HuedTile[] GetStaticTiles( int x, int y ) {
      HuedTile[][][] tiles = GetStaticBlock( x >> 3, y >> 3 );

      return tiles[x & 0x7][y & 0x7];
    }

    public void SetLandBlock( int x, int y, Tile[] value ) {
      if ( x < 0 || y < 0 || x >= this.m_BlockWidth || y >= this.m_BlockHeight )
        return;

      if ( this.m_LandTiles[x] == null )
        this.m_LandTiles[x] = new Tile[this.m_BlockHeight][];

      this.m_LandTiles[x][y] = value;
    }

    public Tile[] GetLandBlock( int x, int y ) {
      if ( x < 0 || y < 0 || x >= this.m_BlockWidth || y >= this.m_BlockHeight || this.m_Map == null )
        return this.m_InvalidLandBlock;

      if ( this.m_LandTiles[x] == null )
        this.m_LandTiles[x] = new Tile[this.m_BlockHeight][];

      Tile[] tiles = this.m_LandTiles[x][y];

      if ( tiles == null )
        tiles = this.m_LandTiles[x][y] = ReadLandBlock( x, y );

      return tiles;
    }

    public Tile GetLandTile( int x, int y ) {
      Tile[] tiles = GetLandBlock( x >> 3, y >> 3 );

      return tiles[( ( y & 0x7 ) << 3 ) + ( x & 0x7 )];
    }

    private unsafe HuedTile[][][] ReadStaticBlock( int x, int y ) {
      this.m_IndexReader.BaseStream.Seek( ( ( x * this.m_BlockHeight ) + y ) * 12, SeekOrigin.Begin );

      int lookup = this.m_IndexReader.ReadInt32();
      int length = this.m_IndexReader.ReadInt32();

      if ( lookup < 0 || length <= 0 )
        return this.m_EmptyStaticBlock;
      else {
        int count = length / 7;

        this.m_Statics.Seek( lookup, SeekOrigin.Begin );

        var staTiles = new StaticTile[count];

        fixed ( StaticTile* pTiles = staTiles ) {
          _lread( this.m_Statics.Handle, pTiles, length );

          if ( m_Lists == null ) {
            m_Lists = new HuedTileList[8][];

            for ( int i = 0; i < 8; ++i ) {
              m_Lists[i] = new HuedTileList[8];

              for ( int j = 0; j < 8; ++j )
                m_Lists[i][j] = new HuedTileList();
            }
          }

          HuedTileList[][] lists = m_Lists;

          StaticTile* pCur = pTiles, pEnd = pTiles + count;

          while ( pCur < pEnd ) {
            lists[pCur->m_X & 0x7][pCur->m_Y & 0x7].Add( (short)( ( pCur->m_ID & 0x3FFF ) + 0x4000 ), pCur->m_Hue, pCur->m_Z );
            ++pCur;
          }

          var tiles = new HuedTile[8][][];

          for ( int i = 0; i < 8; ++i ) {
            tiles[i] = new HuedTile[8][];

            for ( int j = 0; j < 8; ++j )
              tiles[i][j] = lists[i][j].ToArray();
          }

          return tiles;
        }
      }
    }

    private unsafe Tile[] ReadLandBlock( int x, int y ) {
      this.m_Map.Seek( ( ( x * this.m_BlockHeight ) + y ) * 196 + 4, SeekOrigin.Begin );

      var tiles = new Tile[64];

      fixed ( Tile* pTiles = tiles ) {
        _lread( this.m_Map.Handle, pTiles, 192 );
      }

      return tiles;
    }

    public void Dispose() {
      if ( this.m_Map != null )
        this.m_Map.Close();

      if ( this.m_Statics != null )
        this.m_Statics.Close();

      if ( this.m_IndexReader != null )
        this.m_IndexReader.Close();
    }
  }


  [StructLayout( LayoutKind.Sequential, Pack = 1 )]
  public struct StaticTile {
    public short m_ID;
    public byte m_X;
    public byte m_Y;
    public sbyte m_Z;
    public short m_Hue;
  }


  [StructLayout( LayoutKind.Sequential, Pack = 1 )]
  public struct HuedTile {
    internal short m_ID;
    internal short m_Hue;
    internal sbyte m_Z;

    public int ID {
      get { return this.m_ID; }
    }

    public int Hue {
      get { return this.m_Hue; }
    }

    public int Z {
      get { return this.m_Z; }
      set { this.m_Z = (sbyte)value; }
    }

    public HuedTile( short id, short hue, sbyte z ) {
      this.m_ID = id;
      this.m_Hue = hue;
      this.m_Z = z;
    }

    public void Set( short id, short hue, sbyte z ) {
      this.m_ID = id;
      this.m_Hue = hue;
      this.m_Z = z;
    }
  }


  [StructLayout( LayoutKind.Sequential, Pack = 1 )]
  public struct Tile : IComparable {
    internal short m_ID;
    internal sbyte m_Z;

    public int ID {
      get { return this.m_ID; }
    }

    public int Z {
      get { return this.m_Z; }
      set { this.m_Z = (sbyte)value; }
    }

    public bool Ignored {
      get { return ( this.m_ID == 2 || this.m_ID == 0x1DB || ( this.m_ID >= 0x1AE && this.m_ID <= 0x1B5 ) ); }
    }

    public Tile( short id, sbyte z ) {
      this.m_ID = id;
      this.m_Z = z;
    }

    public void Set( short id, sbyte z ) {
      this.m_ID = id;
      this.m_Z = z;
    }

    public int CompareTo( object x ) {
      if ( x == null )
        return 1;

      if ( !( x is Tile ) )
        throw new ArgumentNullException();

      var a = (Tile)x;

      if ( this.m_Z > a.m_Z )
        return 1;
      else if ( a.m_Z > this.m_Z )
        return -1;

      ItemData ourData = TileData.ItemTable[this.m_ID & 0x3FFF];
      ItemData theirData = TileData.ItemTable[a.m_ID & 0x3FFF];

      if ( ourData.Height > theirData.Height )
        return 1;
      else if ( theirData.Height > ourData.Height )
        return -1;

      if ( ourData.Background && !theirData.Background )
        return -1;
      else if ( theirData.Background && !ourData.Background )
        return 1;

      return 0;
    }
  }
}